Practice IV. Line effects

– RxJS version

2021-09-12

Inspired by JParticles, I decided to make something similar (click for "super-nodes") using RxJs for practice. The source code concatenated follows, but you can also visit the GitHub repo.

Source code

import { BehaviorSubject, fromEvent, Scheduler, interval } from 'rxjs';
import { map, debounceTime } from 'rxjs/operators';

const canvas = document.querySelector('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const context = canvas.getContext('2d');

const darkGray = colors[0];
const background = '#fffff8';
const lineWidth = 2;

export const colors = [
  '#3b4252',
  '#e1e1e1',
  '#c3c3c3',
  '#a5a5a5',
  '#878787',
  '#696969',
];

const nNodes = (canvas.width * canvas.height) / 5000;
const initialState = initState(nNodes);
const nodes$ = new BehaviorSubject(initialState);

function next({ x, y }) {
  const { nodes } = nodes$.getValue();
  nodes$.next({
    nodes,
    mousePosClicked: {
      x,
      y,
    },
  });
}

function update() {
  const { mousePosClicked, nodes } = nodes$.getValue();
  nodes$.next({ mousePosClicked, nodes: nodes.map(updateNode) });
}

function render() {
  clearCanvas();
  const { mousePosClicked, nodes } = nodes$.getValue();
  nodes.forEach(renderNode(mousePosClicked, nodes));
}

const clicks = fromEvent(canvas, 'click');
clicks.pipe(debounceTime(50), map(getMouseXY)).subscribe({ next });

interval(0, Scheduler.animationFrame).subscribe(render);
interval(Math.round(1000 / 60)).subscribe(update);

function clearCanvas(context) {
  context.fillStyle = '#fffff8';
  context.fillRect(0, 0, window.innerWidth, window.innerHeight);
}

function drawLine(x1, y1, color, x2, y2, context) {
  context.strokeStyle = color;
  context.lineWidth = 5;
  context.beginPath();
  context.moveTo(x1, y1);
  context.lineTo(x2, y2);
  context.stroke();
};

function drawCircle (x, y, color, radius, context) {
  context.fillStyle = color;
  context.beginPath();
  context.arc(x, y, radius, 0, 2 * Math.PI);
  context.fill();
};

function getNeighbors(n) {
  return function (node) {
    checkPoint(node.x, node.y, n.x, n.y, 100);
  };
}

function linkNeighbors(node) {
  return function (n) {
    drawLine(node.x, node.y, colors[node.color], n.x, n.y, context);
  };
}

function renderNode({ x, y }, cs, context) {
  return (node) => {
    if (checkPoint(x, y, node.x, node.y, 200)) {
      drawLine(x, y, 'red', node.x, node.y, context);
    }
    cs.filter(getNeighbors(node)).forEach(linkNeighbors(node));
    drawCircle(node.x, node.y, colors[node.color], node.size, context);
  };
}

function getRandomValue(min, max) {
  return Math.floor(Math.random() * (max - min) + min);
}

function getMouseXY(e) {
  return { x: e.clientX, y: e.clientY };
}

function checkPoint(a, b, x, y, r) {
  const distPoints = (a - x) * (a - x) + (b - y) * (b - y);
  return distPoints < r ** 2;
}

function getRandomValueNoZero(v1, v2) {
  const randomValue = getRandomValue(v1, v2 + 1);
  return randomValue !== 0 ? randomValue : getRandomValueNoZero(v1, v2);
}

function initState(nNodes) {
  return {
    nodes: Array.from({ length: nNodes }, (_) => {
      const val = getRandomValue(1, 5);
      const dx = getRandomValueNoZero(-5, 5);
      const dy = getRandomValueNoZero(-5, 5);
      return {
        x: getRandomValue(-100, window.innerWidth + 100),
        y: getRandomValue(-100, window.innerHeight + 100),
        dx,
        dy,
        color: val,
        size: val,
      };
    }),
    mousePosClicked: {
      x: -1000,
      y: -1000,
    },
  };
}

function clearCanvas() {
  context.fillStyle = background;
  context.fillRect(0, 0, window.innerWidth, window.innerHeight);
}

function drawLine(x1, y1, color, x2, y2) {
  context.strokeStyle = color;
  context.lineWidth = lineWidth;
  context.beginPath();
  context.moveTo(x1, y1);
  context.lineTo(x2, y2);
  context.stroke();
}

function drawCircle(x, y, color, radius) {
  context.fillStyle = color;
  context.beginPath();
  context.arc(x, y, radius, 0, 2 * Math.PI);
  context.fill();
}

function drawLinkNeighbors(node, nodes) {
  for (const maybeNeighborNode of nodes) {
    const isNeighbor = checkPoint(
      node.x,
      node.y,
      maybeNeighborNode.x,
      maybeNeighborNode.y,
      100
    );
    if (isNeighbor) {
      drawLine(
        node.x,
        node.y,
        colors[node.color],
        maybeNeighborNode.x,
        maybeNeighborNode.y
      );
    }
  }
}

function renderNode({ x, y }, nodes) {
  return (node) => {
    if (checkPoint(x, y, node.x, node.y, 200)) {
      drawLine(x, y, darkGray, node.x, node.y);
    }
    drawLinkNeighbors(node, nodes);
    drawCircle(node.x, node.y, colors[node.color], node.size);
  };
}

function updateNode(n) {
  const dx = n.x < 0 || n.x > window.innerWidth ? -n.dx : n.dx;
  const dy = n.y < 0 || n.y > window.innerHeight ? -n.dy : n.dy;
  return {
    ...n,
    dx,
    dy,
    x: n.x + dx,
    y: n.y + dy,
  };
}

About | Archive